package com.xunk.common.redis.lock;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @ProjectName: HIM
 * @Package: com.xunk.common.redis.lock
 * @ClassName: RedisLockImpl
 * @Author: luanzhiwei
 * @Description: redis锁实现类
 * @Date: 2021/4/20 14:50
 * @Version: 1.0
 */
@Component
@Slf4j
public class RedisLock implements IRedisLock, Serializable {

    private final RedisTemplate<String, String> redisTemplate;

    public RedisLock(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public IDistributeLock getLock(String key) {
        return new DistributeLock(key);
    }

    class DistributeLock implements IDistributeLock {
        private static final String lockStr = "local key = KEYS[1]\n" +
                "local lockKey = ARGV[1]\n" +
                "local lockCount = 1\n" +
                "\n" +
                "\n" +
                "local val = redis.call('hget',key,lockKey)\n" +
                "if val then\n" +
                "    redis.call('hincrby',key,lockKey,1)\n" +
                "    return 1\n" +
                "end\n" +
                "if redis.call('exists',key)==0 then\n" +
                "    redis.call('hset',key,lockKey,1)\n" +
                "    redis.call('expire',lockKey,5)\n" +
                "    return 1\n" +
                "end\n" +
                "return 0";

        private static final String unLockStr = "--释放锁\n" +
                "local key = KEYS[1]\n" +
                "local lockKey = ARGV[1]\n" +
                "local value = redis.call('hget',key,lockKey)\n" +
                "if value then\n" +
                "    if tonumber(value)>1 then\n" +
                "        redis.call('hincrby',key,lockKey,-1)\n" +
                "    else\n" +
                "        redis.call('del',key)\n" +
                "    end\n" +
                "end";


        private static final long SLEEP_TIME = 50;

        private final DefaultRedisScript<Long> lockScript = new DefaultRedisScript<>(lockStr, Long.class);

        private final DefaultRedisScript<Long> unLockScript = new DefaultRedisScript<>(unLockStr, Long.class);

        private String key;

        public DistributeLock(String key) {
            this.key = key;
        }

        //尝试加锁，立即返回
        @Override
        public boolean tryLock(String uuid, long leaseTime) {
            Long lc = redisTemplate.execute(lockScript, Arrays.asList(this.key), uuid, String.valueOf(leaseTime));
            log.info(uuid + "尝试加锁");
            return lc != null && lc == 1;
        }

        //自旋锁
        @Override
        public boolean tryLock(String uuid, long leaseTime, long waitTime, TimeUnit timeUnit) {
            if (waitTime < 0) {
                throw new IllegalArgumentException("timeOut is illegal");
            }
            final long allTime = timeUnit.toMillis(waitTime);
            long start = System.currentTimeMillis();
            while (start + allTime >= System.currentTimeMillis()) {
                if (this.tryLock(uuid, leaseTime)) {
                    return true;
                }
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return false;
        }

        @Override
        public void lock(String uuid, long leaseTime) {
            while (true) {
                if (this.tryLock(uuid, leaseTime)) {
                    return;
                }
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void releaseLock(String uuid) {
            redisTemplate.execute(unLockScript, Arrays.asList(this.key), uuid);
            log.info(uuid + "释放锁");
        }
    }
}
